﻿using System;
using System.Net;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
using System.Runtime.Serialization;
using System.Globalization;
using System.Configuration;
using System.ServiceModel.Configuration;
using System.Xml;
using System.Collections.Generic;

namespace Hbrs.Soa.Restclient.Client
{
    #region RestClientBase

    /// <summary>Base class for calling REST-based webservice
    /// which represents its resource as XML entity</summary>
    /// <typeparam name="T">The type of the resource provided by the REST service</typeparam>
    public abstract class RestClientBase<T> where T : class, new()
    {
        #region private attributes

        private Uri baseUri;
        private string itemId;

        #endregion

        #region private member

        /// <summary>Creates a WebRequest for calling the REST based service</summary>
        /// <param name="type">The HTTP verb of the request</param>
        /// <param name="id">The id of the resource</param>
        /// <returns>WebRequest with correct HTTP verb</returns>
        private WebRequest CreateRequest(string type, string id)
        {
            string uri = String.IsNullOrEmpty(id) ? String.Empty : "/" + id;
            WebRequest request = WebRequest.Create(this.baseUri + uri);
            request.Method = type;
            request.ContentType = "application/xml";
            return request;
        }

        /// <summary>Creates a WebRequest for calling the REST based service</summary>
        /// <param name="type">The HTTP verb of the request</param>
        /// <returns>WebRequest with correct HTTP verb</returns>
        private WebRequest CreateRequest(string type)
        {
            return this.CreateRequest(type, null);
        }

        /// <summary>Processes the response of the REST service</summary>
        /// <param name="response">The response of the REST service</param>
        /// <returns>The item sent by the server</returns>
        private T ProcessResponseContent(HttpWebResponse response)
        {
            T item;
            using (Stream resStream = response.GetResponseStream())
            {
                using (XmlReader reader = TransformResponseContent(resStream))
                {
                    XmlSerializer ser = new XmlSerializer(typeof(T));
                    item = (T)ser.Deserialize(reader);
                }
            }
            return item;
        }

        /// <summary>Processes the response of the REST service as a list of items</summary>
        /// <param name="response">The response of the REST service</param>
        /// <returns>The list of items sent by the server</returns>
        /// <remarks>Method processes documents with the following structure:
        /// <code>
        /// &lt;ItemInfoList&gt;<br />
        /// &nbsp;&nbsp;&nbsp; &lt;SampleItem id =&quot;http://someurl.de/items/7263597&quot;&gt;<br />
        /// &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;Value1&gt;Hello World&lt;/Value1&gt;<br />
        /// &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br />
        /// &nbsp;&nbsp;&nbsp; &lt;/SampleItem&gt;<br />
        /// &lt;ItemInfoList&gt;
        /// </code>
        /// </remarks>
        private IDictionary<string, T> ProcessResponseContentList(HttpWebResponse response)
        {
            Dictionary<string, T> items = new Dictionary<string, T>();
            using (Stream resStream = response.GetResponseStream())
            {
                using (XmlReader reader = TransformResponseContentList(resStream))
                {
                    // Ignore list element
                    reader.MoveToContent();
                    string listElementName = reader.Name;
                    bool hasData = reader.Read();
                    if (hasData)
                    {
                        // Deserialize element
                        bool endElementReached = ((reader.NodeType == XmlNodeType.EndElement) && (listElementName == reader.Name));
                        XmlSerializer ser = new XmlSerializer(typeof(T));
                        ser.UnknownAttribute += new XmlAttributeEventHandler(ResponseContentList_UnknownAttribute);
                        while (!endElementReached)
                        {
                            T item = (T)ser.Deserialize(reader);
                            items.Add(this.itemId, item);
                            endElementReached = ((reader.NodeType == XmlNodeType.EndElement) && (listElementName == reader.Name));
                        }
                        this.itemId = null;
                    }
                }
            }
            return items;
        }

        void ResponseContentList_UnknownAttribute(object sender, XmlAttributeEventArgs e)
        {
            if (e.Attr.Name.ToUpperInvariant() == "ID")
            {
                itemId = e.Attr.Value;
            }
        }

        /// <summary>Processes the item specified before being send to the server</summary>
        /// <param name="request">The request used to transfer the resource</param>
        /// <param name="newItem">The item to transfer to the server</param>
        private static void ProcessPutContent(WebRequest request, T newItem)
        {
            XmlSerializer ser = new XmlSerializer(typeof(T));
            using (MemoryStream ms = new MemoryStream())
            {
                ser.Serialize(ms, newItem);
                Stream requestStream = request.GetRequestStream();
                ms.WriteTo(requestStream);
                requestStream.Close();
            }
        }

        #endregion

        #region protected member

        protected virtual XmlReader TransformResponseContent(Stream response)
        {
            return new XmlTextReader(response);
        }

        protected virtual XmlReader TransformResponseContentList(Stream response)
        {
            return new XmlTextReader(response);
        }

        /// <summary>Adds a new item to the list of resources on the server. If server manages a singleton
        /// resource, this method raises an error if an item already exists on the server</summary>
        /// <param name="newItem">The new item to add to the collection</param>
        /// <returns>The item added to the resource collection and sent back by the server</returns>
        protected virtual T OnAddItem(T newItem)
        {
            WebRequest request = CreateRequest("POST");
            ProcessPutContent(request, newItem);

            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            T item;
            if (response.StatusCode == HttpStatusCode.OK
                || response.StatusCode == HttpStatusCode.Created)
            {
                item = ProcessResponseContent(response);
            }
            else
            {
                throw (new RestclientException("Request could not be finished for resource.")
                {
                    RequestMethod = "POST",
                    StatusCode = response.StatusCode,
                    ResourceType = typeof(T)
                });
            }
            return item;
        }

        /// <summary>Deletes an item on the server</summary>
        protected virtual void OnDeleteItem()
        {
            this.OnDeleteItem(null);
        }

        /// <summary>Deletes an item on the server</summary>
        protected virtual void OnDeleteItem(string id)
        {
            WebRequest request = CreateRequest("DELETE", id);
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            if (response.StatusCode != HttpStatusCode.OK)
            {
                throw (new RestclientException("Request could not be finished for resource.")
                {
                    RequestMethod = "DELETE",
                    StatusCode = response.StatusCode,
                    ResourceType = typeof(T)
                });
            }
        }

        /// <summary>Gets one specific item from the server identified by the specified id</summary>
        /// <returns>The item sent by the server</returns>
        protected virtual T OnGetItem()
        {
            return this.OnGetItem(null);
        }

        /// <summary>Gets one specific item from the server identified by the specified id</summary>
        /// <returns>The item sent by the server</returns>
        protected virtual T OnGetItem(string id)
        {
            WebRequest request = CreateRequest("GET", id);
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            T item;
            if (response.StatusCode == HttpStatusCode.OK)
            {
                item = ProcessResponseContent(response);
            }
            else
            {
                throw (new RestclientException("Request could not be finished for resource.")
                {
                    RequestMethod = "GET",
                    StatusCode = response.StatusCode,
                    ResourceType = typeof(T)
                });
            }
            return item;
        }

        /// <summary>Gets one specific item from the server identified by the specified id</summary>
        /// <returns>The item sent by the server</returns>
        protected virtual IDictionary<string, T> OnGetItems()
        {
            WebRequest request = CreateRequest("GET");
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            IDictionary<string, T> items;
            if (response.StatusCode == HttpStatusCode.OK)
            {
                items = ProcessResponseContentList(response);
            }
            else
            {
                throw (new RestclientException("Request could not be finished for resource.")
                {
                    RequestMethod = "GET",
                    StatusCode = response.StatusCode,
                    ResourceType = typeof(T)
                });
            }
            return items;
        }

        /// <summary>Updates a specific item identified by the
        /// specified id and replaces it by the spcified value</summary>
        /// <param name="newItem">The new item to replace the existing item with</param>
        /// <returns>The updated item sent by the server</returns>
        protected virtual T OnUpdateItem(T newItem)
        {
            return OnUpdateItem(null, newItem);
        }

        /// <summary>Updates a specific item identified by the
        /// specified id and replaces it by the spcified value</summary>
        /// <param name="newItem">The new item to replace the existing item with</param>
        /// <returns>The updated item sent by the server</returns>
        protected virtual T OnUpdateItem(string id, T newItem)
        {
            WebRequest request = CreateRequest("PUT", id);
            ProcessPutContent(request, newItem);

            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            T item;
            if (response.StatusCode == HttpStatusCode.OK
                || response.StatusCode == HttpStatusCode.Created)
            {
                item = ProcessResponseContent(response);
            }
            else
            {
                throw (new RestclientException("Request could not be finished for resource.")
                {
                    RequestMethod = "PUT",
                    StatusCode = response.StatusCode,
                    ResourceType = typeof(T)
                });
            }
            return item;
        }

        #endregion

        #region constructor, destructor

        /// <summary>Initializes a new instance of the class</summary>
        public RestClientBase()
        {
        }

        /// <summary>Initializes a new instance of the class</summary>
        /// <param name="serviceName">The name of the referenced service</param>
        public RestClientBase(string serviceName)
        {
            ClientSection clientSection = ConfigurationManager.GetSection("system.serviceModel/client") as ClientSection;
            var targetep = (from ChannelEndpointElement endpoint in clientSection.Endpoints where endpoint.Name == serviceName select endpoint);
            switch (targetep.Count())
            {
                case 0:
                    throw (new ConfigurationErrorsException("No service named " + serviceName + " found."));
                case 1:
                    this.baseUri = targetep.First().Address;
                    break;
                default:
                    throw (new ConfigurationErrorsException("No unique service named " + serviceName + " found."));
            }
        }

        /// <summary>Initializes a new instance of the class</summary>
        /// <param name="baseUr">The base-uri for the resouces of the service</param>
        public RestClientBase(Uri baseUri)
        {
            this.baseUri = baseUri;
        }

        #endregion
    }

    #endregion

    #region RestClientException

    [global::System.Serializable]
    sealed class RestclientException : Exception
    {
        #region contructor, destructor

        public RestclientException() { }
        public RestclientException(string message) : base(message) { }
        public RestclientException(string message, Exception inner) : base(message, inner) { }
        protected RestclientException(
          System.Runtime.Serialization.SerializationInfo info,
          System.Runtime.Serialization.StreamingContext context)
            : base(info, context) { }

        #endregion

        #region public member

        public string RequestMethod { get; set; }
        public HttpStatusCode StatusCode { get; set; }
        public Type ResourceType { get; set; }

        #endregion

        #region public methods

        public override string ToString()
        {
            string msg = String.Format(CultureInfo.InvariantCulture, "For resource type {0}, method: {1}, status: {2}", this.ResourceType, this.RequestMethod, this.StatusCode);
            return msg + base.ToString();
        }

        #endregion
    }

    #endregion
}
